공개 래퍼(Wrapper)와 내부 코어(Core) 분리 패턴 — 언더스코어(__) 관행과 C 표준 예약어
NOTE
C 언어 시스템 프로그래밍과 대규모 코드베이스에서 흔히 볼 수 있는
func()와__func()의 2단계 함수 분리 패턴을 살펴본다. 유효성 검증과 코어 로직의 분리, 인자 추상화, 리눅스 커널의 더블 언더스코어(__) 관행, 그리고 ISO C 표준의 예약어 규칙을 정리한다.
0. 왜 함수를 func()와 __func() 둘로 나누는가?
대형 C 프로젝트나 시스템 라이브러리를 열어보면 동일한 이름 앞에 언더스코어가 2개 붙은 함수 쌍을 자주 마주치게 됩니다:
/* 1. 외부 공개 래퍼 함수 (Public Wrapper) */
int resource_queue_push(int mem_type, int index)
{
// [검사] 파라미터 유효 범위 체크
if (mem_type < 0 || mem_type >= MAX_MEM_TYPES) {
LOG_ERR("잘못된 메모리 타입: %d\n", mem_type);
return FAIL;
}
// [로깅 및 모니터링]
LOG_DBG("Pushing resource into type %d\n", mem_type);
// [위임] 주소를 꺼내 내부 코어 함수로 넘김
return __resource_queue_push(&g_resource_queues[mem_type], index);
}
/* 2. 내부 전용 코어 함수 (Internal Core Worker) */
static int __resource_queue_push(resource_queue_t *queue, int index)
{
// 순수 큐 삽입 알고리즘만 초고속으로 수행 (중복 검사 없음)
return queue_put(queue->mpq, (void *)(long)index);
}이 패턴은 **“호출자의 편의와 안전을 보장하는 문지기(Wrapper)“**와 **“검증이 끝난 데이터를 받아 순수 알고리즘만 수행하는 일꾼(Core)“**을 명확히 분리하는 객체지향의 캡슐화 기법을 C 언어로 구현한 것입니다.
1. 문지기(Wrapper) vs 진짜 일꾼(Core)의 역할 분담
| 구분 | 외형 래퍼 함수 (func) | 내부 코어 함수 (__func) |
|---|---|---|
| 스코프 | 헤더 파일(.h)에 노출되는 공개 API | 파일 내부(.c)에 한정된 static 함수 |
| 주요 책임 | • 인자 유효성 검사 (Sanity Check) • 경계값(Bounds) 및 NULL 체크 • 디버그 로깅 및 모니터링 통계 수집 • 전역 배열/컨텍스트에서 실제 구조체 주소 추출 | • 순수 알고리즘 수행 (Enqueue, Dequeue, Memory Copy 등) • 불필요한 검증 오버헤드 배제 (초고속 연산) |
| 호출 가정 | ”호출자가 넘긴 인자는 언제든 쓰레기값일 수 있다" | "이미 상위에서 검증되었으므로 포인터는 100% 유효하다” |
2. 인자(Signature)의 변화: 관심사의 분리 (Separation of Concerns)
두 함수의 인자 형태가 바뀌는 점이 이 패턴의 백미입니다:
- 외부 호출자 관점:
resource_queue_push(int mem_type, int index)- 호출자는 내부 전역 배열 구조(
g_resource_queues)나 복잡한 포인터 체계를 알 필요가 없습니다. - 단지 번호(enum이나 int)만 던지면 되므로 모듈 간 결합도(Coupling)가 낮아집니다.
- 내부 코어 관점:
__resource_queue_push(resource_queue_t *queue, int index)- 코어 함수는 자신이 어떤 전역 배열의 몇 번째 타입인지 몰라도 됩니다.
- 단지 넘겨받은 구조체 포인터(
queue)에index를 집어넣는 단일 책임(Single Responsibility)만 갖습니다.
3. 리눅스 커널(Linux Kernel)의 고전적 __ 네이밍 관행
더블 언더스코어(__)를 내부 코어 함수에 붙이는 관례는 리눅스 커널 소스코드에서 유래하여 시스템 C 개발자들에게 표준 관용구로 정착되었습니다.
| 안전장치 포함 함수 | 내부 코어 / Unchecked 함수 | 리눅스 커널에서의 실제 차이 |
|---|---|---|
kmalloc(...) | __kmalloc(...) | kmalloc은 플래그 검증 및 캐시 선택 후 __kmalloc 코어 호출 |
copy_to_user(...) | __copy_to_user(...) | 일반 함수는 유저 주소 검증(access_ok) 수행, __는 검증 생략 |
spin_lock(...) | __spin_lock(...) | 인터럽트 상태 플래그 처리 유무의 차이 |
시스템 프로그래머들 사이에서 __ 접두어는 다음과 같은 강력한 경고 신호입니다:
“이 함수(
__xxx)는 안전장치(파라미터 검사, 주소 검증, 락 획득)가 생략된 원시(Raw) 함수이므로, 외부에서 직접 부르지 말고 반드시 상위 래퍼 함수(xxx)를 통해 부르시오!“
4. C 표준(ISO C) 상식 팁: 예약어(Reserved Identifier) 규칙
이 패턴을 작성할 때 반드시 알아두어야 할 C 언어 표준 규약이 있습니다.
ISO C 표준 (ISO/IEC 9899, 7.1.3절)
C 표준 규격에 따르면 다음 식별자들은 컴파일러와 표준 라이브러리(glibc 등)를 위해 영구 예약되어 있습니다:
__(더블 언더스코어)로 시작하는 모든 식별자:- 예:
__func_name,__aligned__,__THROW,__attribute__
- 예:
_(단일 언더스코어) 뒤에 대문자가 오는 식별자:- 예:
_FILE_OFFSET_BITS,_IO_
- 예:
- 파일 스코프에서
_(단일 언더스코어)로 시작하는 식별자:- 예:
_sys_errlist
- 예:
C 표준 관점에서의 엄격한 해석
사용자가
__nrf_util_index_queue_push처럼__로 시작하는 이름을 직접 정의하면, 이론적으로는 컴파일러의 내장 함수(Built-in)나 표준 라이브러리의 매크로와 이름이 충돌(Name Collision)할 위험이 있습니다.
실무 관행 vs 현대적 대안
| 구분 | 형태 | 장단점 및 권장 환경 |
|---|---|---|
| 전통적 리눅스 커널 스타일 | __func_name() | • 직관적으로 “내부/Raw 함수”임을 누구나 알아봄 • 통신/임베디드/OS 개발 실무에서 압도적으로 널리 쓰임 • C 표준 예약어 규칙과는 형식상 상충 |
| 모던 C 표준 준수 스타일 | func_name_internal() 또는 do_func_name() | • C 표준 100% 준수 (예약어 충돌 위험 제로) • 함수 목적이 영문으로 명확히 드러남 |
| 접미사 언더스코어 | func_name_() | • 표준과 충돌하지 않으면서 C++ 스타일의 내부 식별자 관례 유지 |
5. 실무 적용 시 핵심 체크리스트
- 내부 코어 함수는 반드시
static으로 선언할 것:__func는 파일 외부로 심볼이 노출되면 안 됩니다. 컴파일 단위(.c) 내부로 스코프를 제한하여 외부 직접 호출을 원천 봉쇄합니다.
- 동일 모듈 내부에서의 재사용 이점:
- 모듈 내부의 다른 함수가 이미
pst_queue포인터를 확보하고 있다면, 굳이mem_type으로 다시 배열 조회를 거치는 래퍼를 부르지 않고__func(pst_queue, index)를 직접 호출하여 불필요한 조회를 생략할 수 있습니다.
- 모듈 내부의 다른 함수가 이미
- 관심사의 분리 준수:
- 래퍼 함수에는 비즈니스 핵심 알고리즘을 넣지 말고 검증/로깅만 담당하게 합니다.
- 코어 함수에는
if (param == NULL)같은 방어 코드를 중복 작성하지 않아 코드 가독성과 성능을 극대화합니다.